/*
 * Copyright the original author or authors.
 * 
 * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.mozilla.org/MPL/MPL-1.1.html
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.kairos.commands
{
	import flash.events.Event;
	
	import com.bourre.collection.HashMap;
	import com.bourre.commands.Command;
	import com.bourre.error.NullPointerException;
	import com.bourre.error.UnsupportedOperationException;
	import com.bourre.log.PixlibStringifier;
	import com.kairos.engine.RTAnimation;
	import com.kairos.engine.RTBeacon;	

	/**
	 * Extends behavior of the <code>CommandMS</code> object of LowRA with Kairos
	 * functionalities such time scaling. 
	 * <p>
	 * All behaviors of the <code>CommandRT</code> are performed according
	 * to the <code>RTAnimation</code> interface of Kairos. That's mean that you
	 * can start/stop the execution of commands, regulate the time flow or switching
	 * the beacon for all interval managed by the <code>CommandRT</code>.
	 * </p><p>
	 * There's some differences between <code>CommandMS</code> and <code>CommandRT</code>
	 * behaviors, due to the specifity of time within Kairos. All differences are documented
	 * for each different methods in their documentation.
	 * </p><p>
	 * Notice that because the <code>RTAnimation</code> interface also defines a 
	 * <code>stop</code> method, the <code>CommandMS.stop</code> method is replaced by
	 * the <code>stopCommand</code> method.
	 * </p>	 * 
	 * @author Cédric Néhémie
	 */
	public class CommandRT implements RTAnimation
	{
		protected var _oBeacon : RTBeacon;
		protected var _nLocalSpeed : Number;
		protected var _oData : HashMap;
		protected var _bIsRunning : Boolean;
		
		private var _nID : Number;
		
		protected static var EXT : String = "_C_";
		
		/**
		 * Creates a new <code>CommandRT</code> which is empty and stopped
		 * at creation.
		 * <p>
		 * When <code>CommandMS</code> stores customs objects and use 
		 * native interval created with the <code>setInterval</code> method,
		 * the <code>CommandRT</code> stores <code>IntervalRT</code> instance
		 * instead.
		 * </p><p>
		 * Another big difference with <code>CommandMS</code> is that the
		 * RT one is not playing at creation, even if you add commands, you
		 * have to explicitely call the <code>start</code> method in order
		 * to the object to run. You could use the <code>autoStart</code>
		 * parameters of the constructor to automatically start the command at
		 * creation.
		 * </p> 
		 * @param autoStart	<code>Boolean</code> that indicates if the object 
		 * 					starts at creation.
		 */
		public function CommandRT( autoStart : Boolean = false )
		{
			_oData = new HashMap();
			_nLocalSpeed = 1;
			_oBeacon = RTBeacon.getInstance();
			_bIsRunning = false;
			_nID = 0;
			
			if( autoStart ) start();
		}
		
		/*-----------------------------------------------------------------
				RTAnimation IMPLEMENTATION
		------------------------------------------------------------------*/
		
		/**
		 * Defines on which beacon instance the object will run.
		 * <p>
		 * The call of the <code>setTickBeacon</code> method is relayed
		 * on each internal interval. In that way all executions are allways
		 * running in the same referential that the <code>CommandRT</code>.
		 * </p><p>
		 * Use that method if you want to trigger commands indepentely
		 * of the global <code>RTBeacon</code> instance.
		 * </p><p>
		 * If the passed-in beacon is null the object have to throw
		 * a <code>NullPointerException</code> error.
		 * </p>
		 * @param 	beacon <code>RTBeacon</code> or subclass instance.
		 * @throws 	<code>NullPointerException</code> — The passed-in beacon is null.
		 */
		public function setTickBeacon( beacon : RTBeacon ) : void
		{
			if( !beacon ) 
		 		throw new NullPointerException ( "Trying to set a null beacon for " + this );
		
		 	if( _bIsRunning )
		  	{ 
		  		stop();
		  		_oBeacon = beacon;
		  		start();
		  	}
		  	else
		  	{
		 		_oBeacon = beacon;
		  	}
			
			var values : Array = _oData.getValues();
			var l:Number = values.length;
			
			while( l-- )
			{
				( values[ l ] as IntervalRT ).setTickBeacon( beacon );
			}
		}
		
		/**
		 * Returns the current beacon onto which the object run.
		 * 
		 * @return  <code>RTBeacon</code> used by the object.
		 */
		public function getTickBeacon() : RTBeacon 
		{
			return _oBeacon;
		}
		
		/**
		 * Defines the local speed factor for the current object. Local speed works
		 * as a multiplier for the time step value received from the beacon - as the
		 * <code>setGlobalSpeed</code> feature of the RTBeacon.
		 * <p>
		 * The call of the <code>setLocalSpeed</code> method is relayed
		 * on each internal interval. In that way all executions are allways
		 * running in the same referential that the <code>CommandRT</code>.
		 * </p> 
		 * @param 	rate Float number used as multiplier for the current time step
		 * @throws 	<code>RangeError</code> — The value specified in <code>setLocalSpeed</code>
		 * 			isn't supported by the current object.	
		 */
		public function setLocalSpeed ( rate : Number ) : void
		{
			_nLocalSpeed = rate;
			
			var values : Array = _oData.getValues();
			var l:Number = values.length;
			
			while( l-- )
			{
				( values[ l ] as IntervalRT ).setLocalSpeed( rate );
			}
		}
		
		/**
		 * Returns the current local speed factor of the object.
		 * 
		 * @return <code>Number</code> local speed of the object.
		 */
		public function getLocalSpeed () : Number
		{
			return _nLocalSpeed;
		}
		
		/**
		 * Starts the animation by register it to listen to the beacon
		 * <code>onTick</code> event.
		 * <p>
		 * Any call of the <code>start</code> method when the object
		 * is playing have to be ignored.
		 * </p><p>
		 * The call of the <code>start</code> method is relayed
		 * on each internal interval.
		 * </p>
		 */
		public function start () : void
		{
			if( !_bIsRunning )
			{
				_bIsRunning = true;
				_oBeacon.addTickListener( this );
				
				var values : Array = _oData.getValues();
				var l:Number = values.length;
				
				while( l-- )
				{
					( values[ l ] as IntervalRT ).start();
				}
			}
		}
		
		/**
		 * Stops the animation by register it to listen to the beacon
		 * <code>onTick</code> event.
		 * <p>
		 * Any call of the <code>stop</code> method when the object
		 * isn't playing have to be ignored.
		 * </p><p>
		 * The call of the <code>stop</code> method is relayed
		 * on each internal interval.
		 * </p>
		 */
		public function stop() : void
		{
			if( _bIsRunning )
			{
				_bIsRunning = false;
				_oBeacon.removeTickListener( this );
			
				var values : Array = _oData.getValues();
				var l:Number = values.length;
				
				while( l-- )
				{
					( values[ l ] as IntervalRT ).stop();
				}
			}
		}
		
		/**
		 * Unsupported method
		 */		
		public function reset () : void
		{
			throw new UnsupportedOperationException ( "The reset method of the Suspendable interface is not supported by CommandRT" );
		}
		
		/**
		 * Returns <code>true</code> if the animation is playing, either <code>false</code>.
		 * <p>
		 * An object is considered as running if it have been plugged on the beacon, even
		 * if the beacon is stop itself. So use the <code>RTBeacon.stop</code> with care if
		 * you use it at the same time of any other time control solution.
		 * </p> 
		 * @return <code>true</code> if the animation is playing, either <code>false</code>.
		 */
		public function isRunning () : Boolean
		{
			return _bIsRunning;
		}
		
		/**
		 * Start alias
		 * 
		 * @see	#start()
		 */		
		public function run () : void
		{
			start();
		}
		
		/**
		 * Do nothing.
		 */
		public function onTick( e : Event = null ) : void {}
		
		/*-----------------------------------------------------------------
				PUBLIC METHODS
		------------------------------------------------------------------*/
		
		/**
		 * Adds a command to the <code>CommandRT</code> to be executed indefinitely
		 * each <code>nMs</code> mlliseconds.
		 * <p>
		 * When adding the command a unique id is defined for that command, that
		 * id is returned by the method.
		 * </p>
		 * @param oC  <code>Command</code> instance to execute.
		 * @param nMs <code>Number</code> of milliseconds between each execution of the command.
		 * @return <code>String</code> id at which the command have been registered.
		 */
		public function push( oC : Command, nMs : Number ) : String
		{
			var s : String = _getNameID();
			
			_remove( s );
			
			return _push( oC, nMs, _getNameID() );
		}
		
		/**
		 * Adds a command to the <code>CommandRT</code> at the specified id
		 * to be executed indefinitely each <code>nMs</code> mlliseconds.
		 * <p>
		 * When adding the command is the id is already defined, the interval stored
		 * at the id is stopped and removed.
		 * </p> 
		 * @param oC  <code>Command</code> instance to execute.
		 * @param nMs <code>Number</code> of milliseconds between each execution of the command.
		 * @param sN  <code>String</code> id at which register the command.
		 * @return <code>String</code> id at which the command have been registered.
		 */
		public function pushWithName( oC : Command, nMs : Number, sN : String = null ) : String
		{
			if( sN == null ) sN = _getNameID();
			
			_remove( sN );
			
			return _push( oC, nMs, sN );
		}
		
		/**
		 * Adds a command to the <code>CommandRT</code> to be executed only one time
		 * after <code>nMs</code> mlliseconds.
		 * <p>
		 * When adding the command a unique id is defined for that command, that
		 * id is returned by the method.
		 * </p>
		 * @param oC  <code>Command</code> instance to execute.
		 * @param nMs <code>Number</code> of milliseconds before execution of the command.
		 * @return <code>String</code> id at which the command have been registered.
		 */
		public function delay( oC : Command, nMs : Number ) : String
		{
			var s : String = _getNameID();
			
			_remove( s );
			
			return _delay( oC, nMs, _getNameID() );
		}
		
		
		/**
		 * Adds a command to the <code>CommandRT</code> at the specified id
		 * to be executed only one time after <code>nMs</code> mlliseconds.
		 * <p>
		 * When adding the command is the id is already defined, the interval stored
		 * at the id is stopped and removed.
		 * </p>
		 * @param oC  <code>Command</code> instance to execute.
		 * @param nMs <code>Number</code> of milliseconds before execution of the command.
		 * @param sN  <code>String</code> id at which register the command.
		 * @return <code>String</code> id at which the command have been registered.
		 */
		public function delayWithName( oC : Command, nMs : Number, sN : String = null ) : String
		{
			if( sN == null ) sN = _getNameID();
			
			_remove( sN );
			
			return _delay( oC, nMs, sN );
		}
		
		/**
		 * Stops and removes the specified command if found in the current object.
		 * 
		 * @param oC <code>Command</code> to remove and stop.
		 * @return <code>true</code> if the command have been removed,
		 * 		   either <code>false</code>
		 */
		public function remove( oC : Command ) : Boolean
	  	{
	  		var key : String = _findCommand( oC );
			return key == null ? false : _remove( key );
	  	}
		
		/**
		 * Stops and removes the command stored at the specified id 
		 * if found in the current object.
		 * 
		 * @param sN <code>String</code> id of the command to remove and stop.
		 * @return <code>true</code> if the command have been removed,
		 * 		   either <code>false</code>
		 */
		public function removeWithName( sN : String ) : Boolean
		{
			return _remove( sN );
		}
		
		/**
		 * Stops the specified command if found in the current object.
		 * 
		 * @param oC <code>Command</code> to stop.
		 * @return <code>true</code> if the command have been stopped,
		 * 		   either <code>false</code>
		 */
		public function stopCommand( oC : Command ) : Boolean
		{
			var key : String = _findCommand( oC );
			return key == null ? false : _stop( key );
		}

		/**
		 * Stops the command stored at the specified id 
		 * if found in the current object.
		 * 
		 * @param sN <code>String</code> id of the command to stop.
		 * @return <code>true</code> if the command have been stopped,
		 * 		   either <code>false</code>
		 */
		public function stopWithName( sN : String ) : Boolean
		{
			return _stop( sN );
		}
		
		/**
		 * Restarts the specified command if found in the current object.
		 * 
		 * @param oC <code>Command</code> to restart.
		 * @return <code>true</code> if the command have been resumed,
		 * 		   either <code>false</code>
		 */
		public function resume( oC : Command ) : Boolean
		{
			var key : String = _findCommand( oC );
			return key == null ? false : _resume( key );
		}
		
		/**
		 * Restarts the command stored at the specified id 
		 * if found in the current object.
		 * 
		 * @param sN <code>String</code> id of the command to restart.
		 * @return <code>true</code> if the command have been resumed,
		 * 		   either <code>false</code>
		 */
		public function resumeWithName( sN : String ) : Boolean
		{
			return _resume( sN );
		}
		
		/**
		 * Returns the number of commands registered in the the current object.
		 * 
		 * @return <code>Number</code> of commands stored in that object.
		 */
		public function getLength() : Number
		{
			return _oData.size();
		}
		
		/**
		 * Stops and removes all commands registered in the current object
		 */
		public function removeAll() : void
		{
			stop();
			_oData = new HashMap();
		}
		
		/**
		 * Returns the string representation of the object.
		 * 
		 * @return <code>String</code> representation of the object.
		 * 
		 */
		public function toString() : String 
		{
			return PixlibStringifier.stringify( this );
		}
		
		/*-----------------------------------------------------------------
				PROTECTED METHODS
		------------------------------------------------------------------*/

		/**
		 * Realizes the concret action of inserting the command's call in the object.
		 * <p>
		 * The <code>_push</code> create an <code>IntervalRT</code> instance.
		 * </p>
		 * @param oC  <code>Command</code> instance to execute.
		 * @param nMs <code>Number</code> of milliseconds before execution of the command.
		 * @param sN  <code>String</code> id at which register the command.
		 * @return <code>String</code> id at which the command have been registered.
		 */
		protected function _push( oC : Command, nMs : Number, sN : String) : String
		{			
			var interval : IntervalRT = new IntervalRT( oC, nMs );
			interval.setTickBeacon( _oBeacon );
			interval.setLocalSpeed( _nLocalSpeed );
			
			_oData.put( sN, interval );
			
			if( isRunning() )
			{
				interval.start();
				oC.execute();
			}
			return sN;
		}
		
		/**
		 * Realizes the concret action of inserting the command's call in the object.
		 * <p>
		 * The <code>_delay</code> create a <code>TimeoutRT</code> instance.
		 * </p>
		 * @param oC  <code>Command</code> instance to execute.
		 * @param nMs <code>Number</code> of milliseconds before execution of the command.
		 * @param sN  <code>String</code> id at which register the command.
		 * @return <code>String</code> id at which the command have been registered.
		 */
		protected function _delay( oC : Command, nMs : Number, sN:String ) : String
		{
			var interval : IntervalRT = new TimeoutRT( oC, nMs );
			interval.setTickBeacon( _oBeacon );
			interval.setLocalSpeed( _nLocalSpeed );
			
			_oData.put( sN, interval );
			
			if( isRunning() )
			{
				interval.start();
				oC.execute();
			}
			return sN;
		}
		
		/**
		 * Realizes the concret action of removing a command
		 * from the object if the passed-in id is found.
		 * 
		 * @param s	<code>String</code> id associated with the command to remove
		 * @return <code>true</code> if the command have been successfully
		 * 		   removed, either <code>false</code>
		 */
		protected function _remove(s:String) : Boolean
		{
			if( _oData.containsKey( s ) )
			{
				( _oData.get ( s ) as IntervalRT ).stop();
				_oData.remove( s );
				return true;
			}
			return false;
		}
		
		/**
		 * Realizes the concret action of stopping a command
		 * from the object if the passed-in id is found.
		 * 
		 * @param s	<code>String</code> id associated with the command to stop
		 * @return <code>true</code> if the command have been successfully
		 * 		   stopped, either <code>false</code>
		 */
		protected function _stop(s:String) : Boolean
		{
			if( _oData.containsKey( s ) )
			{
				( _oData.get ( s ) as IntervalRT ).stop();
				return true;
			}
			return false;
		}
		
		/**
		 * Realizes the concret action of resuming a command
		 * from the object if the passed-in id is found.
		 * 
		 * @param s	<code>String</code> id associated with the command to stop
		 * @return <code>true</code> if the command have been successfully
		 * 		   resumed, either <code>false</code>
		 */
		protected function _resume(s:String) : Boolean
		{
			if( _oData.containsKey( s ) )
			{
				( _oData.get ( s ) as IntervalRT ).start();
				return true;
			}
			return false;
		}
		
		/**
		 * Returns the id associated with the passed-in command, if found
		 * in the current object.
		 * 
		 * @param oC <code>Command</code> to find in the object.
		 * @return <code>String</code> id associated with the
		 * 		   object or <code>null</code>
		 */
		protected function _findCommand ( oC : Command ) : String
		{
			var keys : Array = _oData.getKeys();
			var l : Number = keys.length;
			while( l-- )
			{
				var interval : IntervalRT = _oData.get( keys[ l ] );
				if( oC == interval.getCommand() )
					return keys[ l ];
			}
			return null;
		}
		
		/**
		 * Returns a new unique id.
		 * 
		 * @return <code>String</code> unique id. 
		 */
		protected function _getNameID() : String
		{
			return EXT + _nID++;
		}
	}
}